package com.wcs.exam.controller.auth.biz;

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.tencentcloudapi.faceid.v20180301.models.DetectAuthResponse;
import com.tencentcloudapi.faceid.v20180301.models.DetectInfoText;
import com.tencentcloudapi.faceid.v20180301.models.GetDetectInfoEnhancedResponse;
import com.tencentcloudapi.faceid.v20180301.models.LivenessCompareResponse;
import com.tencentcloudapi.iai.v20200303.models.CompareFaceResponse;
import com.wcs.exam.common.base.BaseBiz;
import com.wcs.exam.common.base.Result;
import com.wcs.exam.common.base.exception.BaseException;
import com.wcs.exam.common.base.page.SqlUtil;
import com.wcs.exam.common.cache.CacheRedis;
import com.wcs.exam.common.config.ThreadContext;
import com.wcs.exam.common.constant.RedisConstant;
import com.wcs.exam.common.enums.*;
import com.wcs.exam.common.faceid.FaceContrastUtil;
import com.wcs.exam.common.faceid.bean.FaceQueryRecordResult;
import com.wcs.exam.common.faceid.bean.GetfaceidReturn;
import com.wcs.exam.common.server.upload.Upload;
import com.wcs.exam.common.server.upload.UploadService;
import com.wcs.exam.common.util.ImgUtil;
import com.wcs.exam.controller.auth.req.AuthUserAttendanceCheckReq;
import com.wcs.exam.controller.auth.req.AuthUserAttendanceDetectAuthUrlReq;
import com.wcs.exam.controller.auth.req.AuthUserAttendanceInfoReq;
import com.wcs.exam.controller.auth.resp.AuthUserAttendanceAppParamsResp;
import com.wcs.exam.controller.auth.resp.AuthUserAttendanceCheckResp;
import com.wcs.exam.controller.auth.resp.AuthUserAttendanceDetectAuthUrlResp;
import com.wcs.exam.controller.auth.resp.AuthUserAttendanceInfoResp;
import com.wcs.exam.controller.biz.SysConfigCommonBiz;
import com.wcs.exam.controller.biz.resp.SysConfigFaceResp;
import com.wcs.exam.dao.*;
import com.wcs.exam.dao.impl.mapper.entity.*;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import org.springframework.stereotype.Component;

import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * Auth-用户考勤
 *
 * @author wcs
 */
@Component
@RequiredArgsConstructor
public class AuthUserAttendanceBiz extends BaseBiz {

    @NotNull
    private final UploadService uploadService;
    @NotNull
    private final CacheRedis cacheRedis;

    @NotNull
    private final UsersDao usersDao;
    @NotNull
    private final SysConfigCommonBiz sysConfigCommonBiz;

    @NotNull
    private final UserAttendanceDao dao;
    @NotNull
    private final ExamDao examDao;
    @NotNull
    private final ExamUserDao examUserDao;
    @NotNull
    private final ExamUserRecordDao examUserRecordDao;
    @NotNull
    private final ExamUserAppealDao examUserAppealDao;

    /**
     * 用户考勤-考勤信息查询
     *
     * @param req 请求参数
     * @return 考勤信息
     */
    public Result<AuthUserAttendanceInfoResp> info(AuthUserAttendanceInfoReq req) {
        // 校验业务类型
        AttendanceTypeEnum attendanceTypeEnum = AttendanceTypeEnum.byCode(req.getAttendanceType());
        if (ObjectUtil.isNull(attendanceTypeEnum)) {
            return Result.error("考勤类型错误");
        }

        // 校验用户信息
        Users user = usersDao.getById(ThreadContext.getUserId());
        if (ObjectUtil.isNull(user)) {
            return Result.error("用户信息不存在");
        }

        if (AttendanceTypeEnum.GENERAL_EXAM.equals(attendanceTypeEnum)) {
            // 模拟考试
            return handleGeneralExamInfo(req.getRelateId(), req.getRecordId(), user);
        } else if (AttendanceTypeEnum.REGISTRATION_EXAM.equals(attendanceTypeEnum)) {
            // 正式考试
            return handleRegistrationExamInfo(req.getRelateId(), user);
        } else {
            return Result.error("该考勤类型暂不支持");
        }
    }

    /**
     * 用户考勤-模拟考试打卡信息处理
     *
     * @param examId 考试ID
     * @param user   用户信息
     * @return 打卡信息
     */
    private Result<AuthUserAttendanceInfoResp> handleGeneralExamInfo(Long examId, Long recordId, Users user) {
        Exam exam = examDao.getById(examId);
        if (ObjectUtil.isNull(exam) || ExamTypeEnum.MOCK.getCode().equals(exam.getExamType())) {
            return Result.error("考试不存在");
        }

        SysConfigFaceResp configFace = sysConfigCommonBiz.getFace();

        AuthUserAttendanceInfoResp.FaceConfig faceConfig = new AuthUserAttendanceInfoResp.FaceConfig()
                .setCertification(CertificationStatusEnum.SUCCESS_AUTH.getCode().equals(user.getCertificationStatus()))
                .setEnabledFaceError(configFace.getFaceErrorEnable())
                .setFaceErrorCount(configFace.getFaceErrorLimit())
                .setFaceId(exam.getFaceId())
                .setFaceCapture(exam.getFaceCapture())
                .setFaceCaptureTime(exam.getFaceCaptureTime())
                .setFaceCaptureDetection(exam.getFaceCaptureDetection())
                .setFaceCaptureHighDetection(exam.getFaceCaptureHighDetection())
                .setFaceCaptureFailureCount(exam.getFaceCaptureFailureCount());

        // 考勤方式
        if (Boolean.TRUE.equals(exam.getFaceId())) {
            // 统计当日错误次数
            int faceUserTodayErrorCount = getUserTodayFaceErrorCount(ThreadContext.getUserId());
            faceConfig.setFaceUserTodayErrorCount(faceUserTodayErrorCount);

            if (ObjectUtil.isNotNull(recordId)) {
                // 是否已通过人脸识别
                boolean faceIdCheckPass = dao.lambdaQuery()
                        .eq(UserAttendance::getAttendanceType, AttendanceTypeEnum.GENERAL_EXAM.getCode())
                        .eq(UserAttendance::getRelateId, examId)
                        .eq(UserAttendance::getAttendanceWay, AttendanceWayEnum.FACE_ID.getCode())
                        .eq(UserAttendance::getAttendanceExamScene, AttendanceExamSceneEnum.EXAM_BEFORE.getCode())
                        .eq(UserAttendance::getUserId, ThreadContext.getUserId())
                        .eq(UserAttendance::getId, recordId)
                        .eq(UserAttendance::getAttendancePass, Boolean.TRUE)
                        .exists();
                faceConfig.setFaceIdCheckPass(faceIdCheckPass);

                // 错误次数统计
                Long faceIdUserErrorCount = dao.lambdaQuery()
                        .eq(UserAttendance::getAttendanceType, AttendanceTypeEnum.GENERAL_EXAM.getCode())
                        .eq(UserAttendance::getRelateId, examId)
                        .eq(UserAttendance::getAttendanceWay, AttendanceWayEnum.FACE_ID.getCode())
                        .eq(UserAttendance::getAttendanceExamScene, AttendanceExamSceneEnum.EXAM_BEFORE.getCode())
                        .eq(UserAttendance::getUserId, ThreadContext.getUserId())
                        .eq(UserAttendance::getId, recordId)
                        .eq(UserAttendance::getAttendancePass, Boolean.FALSE)
                        .count();
                faceConfig.setFaceIdUserErrorCount(faceIdUserErrorCount.intValue());
            }
        }

        // 人脸抓拍检测
        if (Boolean.TRUE.equals(exam.getFaceCaptureDetection())) {
            if (ObjectUtil.isNotNull(recordId)) {
                Long faceCaptureUserFailureCount = dao.lambdaQuery()
                        .eq(UserAttendance::getAttendanceType, AttendanceTypeEnum.GENERAL_EXAM.getCode())
                        .eq(UserAttendance::getRelateId, examId)
                        .eq(UserAttendance::getAttendanceWay, AttendanceWayEnum.FACE_ID.getCode())
                        .in(UserAttendance::getAttendanceExamScene, AttendanceExamSceneEnum.EXAM_ANSWER.getCode(), AttendanceExamSceneEnum.EXAM_ANSWER_COMPARE.getCode())
                        .eq(UserAttendance::getUserId, ThreadContext.getUserId())
                        .eq(UserAttendance::getId, recordId)
                        .eq(UserAttendance::getAttendancePass, Boolean.FALSE)
                        .count();
                faceConfig.setFaceCaptureUserFailureCount(faceCaptureUserFailureCount.intValue());
            }
        }

        AuthUserAttendanceInfoResp resp = new AuthUserAttendanceInfoResp()
                .setAttendanceType(AttendanceTypeEnum.GENERAL_EXAM.getCode())
                .setRelateId(examId)
                .setAttendanceWay(AttendanceWayEnum.FACE_ID.getCode())
                .setFaceConfig(faceConfig);
        return Result.success(resp);
    }

    /**
     * 用户考勤-正式考试打卡信息处理
     *
     * @param examId 考试ID
     * @param user   用户信息
     * @return 打卡信息
     */
    private Result<AuthUserAttendanceInfoResp> handleRegistrationExamInfo(Long examId, Users user) {
        Exam exam = examDao.getById(examId);
        if (ObjectUtil.isNull(exam) || ExamTypeEnum.EXAMINATION.getCode().equals(exam.getExamType())) {
            return Result.error("考试不存在");
        }

        SysConfigFaceResp configFace = sysConfigCommonBiz.getFace();

        AuthUserAttendanceInfoResp.FaceConfig faceConfig = new AuthUserAttendanceInfoResp.FaceConfig()
                .setCertification(CertificationStatusEnum.SUCCESS_AUTH.getCode().equals(user.getCertificationStatus()))
                .setEnabledFaceError(configFace.getFaceErrorEnable())
                .setFaceErrorCount(configFace.getFaceErrorLimit())
                .setFaceId(exam.getFaceId())
                .setFaceIdLimitTime(exam.getFaceIdLimitTime())
                .setFaceIdLimitCount(exam.getFaceIdLimitCount())
                .setFaceIdFailureEnter(exam.getFaceIdFailureEnter())
                .setFaceIdFailureAppeal(exam.getFaceIdFailureAppeal())
                .setFaceCapture(exam.getFaceCapture())
                .setFaceCaptureTime(exam.getFaceCaptureTime())
                .setFaceCaptureDetection(exam.getFaceCaptureDetection())
                .setFaceCaptureHighDetection(exam.getFaceCaptureHighDetection())
                .setFaceCaptureFailureCount(exam.getFaceCaptureFailureCount());

        if (Boolean.TRUE.equals(exam.getFaceId())) {
            // 是否已通过人脸识别
            boolean faceIdCheckPass = dao.lambdaQuery()
                    .eq(UserAttendance::getAttendanceType, AttendanceTypeEnum.REGISTRATION_EXAM.getCode())
                    .eq(UserAttendance::getRelateId, examId)
                    .eq(UserAttendance::getAttendanceWay, AttendanceWayEnum.FACE_ID.getCode())
                    .eq(UserAttendance::getAttendanceExamScene, AttendanceExamSceneEnum.EXAM_BEFORE.getCode())
                    .eq(UserAttendance::getUserId, ThreadContext.getUserId())
                    .eq(UserAttendance::getAttendancePass, Boolean.TRUE)
                    .exists();
            faceConfig.setFaceIdCheckPass(faceIdCheckPass);

            // 错误次数统计
            Long faceIdUserErrorCount = dao.lambdaQuery()
                    .eq(UserAttendance::getAttendanceType, AttendanceTypeEnum.REGISTRATION_EXAM.getCode())
                    .eq(UserAttendance::getRelateId, examId)
                    .eq(UserAttendance::getAttendanceWay, AttendanceWayEnum.FACE_ID.getCode())
                    .eq(UserAttendance::getAttendanceExamScene, AttendanceExamSceneEnum.EXAM_BEFORE.getCode())
                    .eq(UserAttendance::getUserId, ThreadContext.getUserId())
                    .eq(UserAttendance::getAttendancePass, Boolean.FALSE)
                    .count();
            faceConfig.setFaceIdUserErrorCount(faceIdUserErrorCount.intValue());

            // 统计当日错误次数
            int faceUserTodayErrorCount = getUserTodayFaceErrorCount(ThreadContext.getUserId());
            faceConfig.setFaceUserTodayErrorCount(faceUserTodayErrorCount);
        } else {
            faceConfig.setFaceIdCheckPass(Boolean.TRUE);
            faceConfig.setFaceIdUserErrorCount(0);
        }

        // 人脸抓拍检测
        if (Boolean.TRUE.equals(exam.getFaceCaptureDetection())) {
            Long faceCaptureUserFailureCount = dao.lambdaQuery()
                    .eq(UserAttendance::getAttendanceType, AttendanceTypeEnum.REGISTRATION_EXAM.getCode())
                    .eq(UserAttendance::getRelateId, examId)
                    .eq(UserAttendance::getAttendanceWay, AttendanceWayEnum.FACE_ID.getCode())
                    .in(UserAttendance::getAttendanceExamScene, AttendanceExamSceneEnum.EXAM_ANSWER.getCode(), AttendanceExamSceneEnum.EXAM_ANSWER_COMPARE.getCode())
                    .eq(UserAttendance::getUserId, ThreadContext.getUserId())
                    .eq(UserAttendance::getAttendancePass, Boolean.FALSE)
                    .count();
            faceConfig.setFaceCaptureUserFailureCount(faceCaptureUserFailureCount.intValue());
        } else {
            faceConfig.setFaceCaptureUserFailureCount(0);
        }

        // 需要完善异常审核信息
        if (ExamTypeEnum.EXAMINATION.getCode().equals(exam.getExamType())) {
            ExamUserAppeal errorAppeal = examUserAppealDao.lambdaQuery()
                    .eq(ExamUserAppeal::getExamId, examId)
                    .eq(ExamUserAppeal::getUserId, ThreadContext.getUserId())
                    .orderByDesc(ExamUserAppeal::getId)
                    .last(SqlUtil.limitOne())
                    .one();
            if (ObjectUtil.isNotNull(errorAppeal)) {
                faceConfig.setAppealStatus(errorAppeal.getAppealStatus())
                        .setAppealCause(errorAppeal.getAppealCause())
                        .setAuditTime(errorAppeal.getAuditTime())
                        .setAuditOpinion(errorAppeal.getAuditOpinion());
            }
        }


        AuthUserAttendanceInfoResp resp = new AuthUserAttendanceInfoResp()
                .setAttendanceType(AttendanceTypeEnum.REGISTRATION_EXAM.getCode())
                .setRelateId(examId)
                .setAttendanceWay(AttendanceWayEnum.FACE_ID.getCode())
                .setFaceConfig(faceConfig);
        return Result.success(resp);
    }

    /**
     * 统计用户当日人脸错误次数
     *
     * @param userId 用户ID
     * @return 用户当日人脸错误次数
     */
    private Integer getUserTodayFaceErrorCount(Long userId) {
        Long count = dao.lambdaQuery()
                .eq(UserAttendance::getAttendanceWay, AttendanceWayEnum.FACE_ID.getCode())
                .ne(UserAttendance::getFaceIdCall, Boolean.TRUE)
                .eq(UserAttendance::getUserId, userId)
                .eq(UserAttendance::getAttendancePass, Boolean.FALSE)
                .ge(UserAttendance::getGmtCreate, LocalDateTime.of(LocalDate.now(), LocalTime.MIN))
                .le(UserAttendance::getGmtCreate, LocalDateTime.of(LocalDate.now(), LocalTime.MAX.withNano(0)))
                .count();
        return count.intValue();
    }

    /**
     * 用户考勤-获取APP参数
     *
     * @return APP参数
     */
    public Result<AuthUserAttendanceAppParamsResp> appParams() {
        Users user = usersDao.getById(ThreadContext.getUserId());
        if (ObjectUtil.isNull(user)) {
            return Result.error("用户信息不存在");
        }
        if (!CertificationStatusEnum.SUCCESS_AUTH.getCode().equals(user.getCertificationStatus())) {
            return Result.error("未完成实名认证");
        }

        SysConfigFaceResp configFace = sysConfigCommonBiz.getFace();
        if (ObjectUtil.isNull(configFace)) {
            return Result.error("人脸识别配置错误");
        }
        if (StrUtil.isBlank(configFace.getTencentFaceWbAppid()) || StrUtil.isBlank(configFace.getTencentFaceSDKLicence())) {
            return Result.error("人脸识别配置错误");
        }

        // 创建返回对象
        AuthUserAttendanceAppParamsResp resp = new AuthUserAttendanceAppParamsResp();
        resp.setUserId(user.getId());
        resp.setFaceContrasImg(user.getBestFrame());
        resp.setOrderNo(IdUtil.simpleUUID());
        resp.setIdCardNo(user.getIdCard());
        resp.setIdCardName(user.getRealName());
        resp.setNonce(RandomUtil.randomString(32));

        // 处理签名
        //获取 Access Token
        String accessToken = FaceContrastUtil.accessToken(configFace.getTencentFaceWbAppid(), configFace.getTencentFaceWbAppidSecret());
        String apiTicket = FaceContrastUtil.apiTicket(configFace.getTencentFaceWbAppid(), "SIGN", null, accessToken);
        String sign = FaceContrastUtil.sign(configFace.getTencentFaceWbAppid(), resp.getUserId().toString(), resp.getNonce(), apiTicket);
        if (StrUtil.isBlank(sign)) {
            return Result.error("人脸对比参数不对，请联系管理员!");
        }

        // 合作方后台上传身份信息
        String imgBase64 = ImgUtil.downloadImgBase64(user.getBestFrame());
        GetfaceidReturn getfaceidReturn = FaceContrastUtil.getFaceId(configFace.getTencentFaceWbAppid(), resp.getOrderNo(), user.getRealName(), user.getIdCard(), user.getId().toString(), imgBase64, "2", sign, resp.getNonce());
        if (ObjectUtil.isNull(getfaceidReturn)) {
            return Result.error("合作方后台上传身份信息，请联系管理员!");
        }
        if (!"0".equals(getfaceidReturn.getCode())) {
            return Result.error(getfaceidReturn.getMsg());
        }

        // 返回信息签名
        String respApiTicket = FaceContrastUtil.apiTicket(configFace.getTencentFaceWbAppid(), "NONCE", resp.getUserId().toString(), accessToken);
        String respSign = FaceContrastUtil.sign(configFace.getTencentFaceWbAppid(), resp.getUserId().toString(), resp.getNonce(), respApiTicket);

        // 处理返回信息
        resp.setFaceId(getfaceidReturn.getResult().getFaceId());
        resp.setWbappid(configFace.getTencentFaceWbAppid());
        resp.setLicence(configFace.getTencentFaceSDKLicence());
        resp.setSign(respSign);
        return Result.success(resp);
    }

    /**
     * 考试人脸记录-获取人脸识别Url
     *
     * @param req 请求参数
     * @return 校验结果
     */
    public Result<AuthUserAttendanceDetectAuthUrlResp> detectAuthUrl(AuthUserAttendanceDetectAuthUrlReq req) {
        // 校验业务类型
        AttendanceTypeEnum attendanceTypeEnum = AttendanceTypeEnum.byCode(req.getAttendanceType());
        if (ObjectUtil.isNull(attendanceTypeEnum)) {
            return Result.error("考勤类型错误");
        }

        // 校验用户信息
        Users user = usersDao.getById(ThreadContext.getUserId());
        if (ObjectUtil.isNull(user)) {
            return Result.error("用户信息不存在");
        }

        if (AttendanceTypeEnum.GENERAL_EXAM.equals(attendanceTypeEnum)) {
            // 模拟考试
            return handleGeneralExamDetectAuthUrl(req, user);
        } else if (AttendanceTypeEnum.REGISTRATION_EXAM.equals(attendanceTypeEnum)) {
            // 正式考试
            return handleRegistrationDetectAuthUrl(req, user);
        } else {
            return Result.error("该考勤类型暂不支持");
        }
    }

    /**
     * 处理模拟考试-实名认证地址
     *
     * @param req  请求参数
     * @param user 用户信息
     * @return 实名认证地址
     */
    private Result<AuthUserAttendanceDetectAuthUrlResp> handleGeneralExamDetectAuthUrl(AuthUserAttendanceDetectAuthUrlReq req, Users user) {
        Exam exam = examDao.getByIdForCache(req.getRelateId());
        if (ObjectUtil.isNull(exam)) {
            return Result.error("考试不存在");
        }

        // 是否购买
        if (BigDecimal.ZERO.compareTo(exam.getPrice()) < 0) {
            ExamUser userGoods = examUserDao.getByExamTypeAndExamIdAndUserId(ExamTypeEnum.MOCK.getCode(), exam.getId(), ThreadContext.getUserId());
            if (Boolean.FALSE.equals(userGoods.getPayment())) {
                return Result.error("考试未购买");
            }
            if (Boolean.FALSE.equals(userGoods.getLongEffective())) {
                if (LocalDateTime.now().isBefore(userGoods.getBeginTime())) {
                    return Result.error("该考试暂未开始，暂时无法进行考试");
                }
                if (LocalDateTime.now().isAfter(userGoods.getEndTime())) {
                    return Result.error("该考试已过期，无法进行考试");
                }
            }
        }

        // 开考前
        if (Boolean.FALSE.equals(exam.getFaceId())) {
            return Result.error("未开启考前人脸识别，无须进行人脸识别");
        }

        // 是否需要填写个人信息
        AuthUserAttendanceDetectAuthUrlResp resp = new AuthUserAttendanceDetectAuthUrlResp();
        resp.setCertification(!CertificationStatusEnum.SUCCESS_AUTH.getCode().equals(user.getCertificationStatus()));
        if (Boolean.TRUE.equals(resp.getCertification())) {
            return Result.success(resp);
        }

        SysConfigFaceResp configFace = sysConfigCommonBiz.getFace();
        // 限制单日最大错误次数
        if (Boolean.TRUE.equals(configFace.getFaceErrorEnable())) {
            Integer errorCount = getUserTodayFaceErrorCount(ThreadContext.getUserId());
            if (errorCount >= configFace.getFaceErrorLimit()) {
                return Result.error("今日达到错误次数超过限制，请明天再试");
            }
        }


        String ruleId = StrUtil.isNotBlank(user.getBestFrame()) ? configFace.getLivenessCompareRuleId() : configFace.getLivenessRecognitionRuleId();
        String imageBase64 = StrUtil.isNotBlank(user.getBestFrame()) ? FaceContrastUtil.encodeBase64File(user.getBestFrame()) : null;

        Long bizId = IdWorker.getId();
        JSONObject extraJson = new JSONObject();
        extraJson.set("bizId", bizId);
        extraJson.set("param", req);

        DetectAuthResponse response = FaceContrastUtil.detectAuth(
                configFace.getTencentFaceSecretId(),
                configFace.getTencentFaceSecretKey(),
                ruleId,
                user.getRealName(),
                user.getIdCard(),
                imageBase64,
                req.getRedirectUrl(),
                extraJson.toString());
        if (ObjectUtil.isNull(response)) {
            return Result.error("获取认证地址失败");
        }

        JSONObject detectAuthJson = new JSONObject();
        detectAuthJson.set("bizToken", response.getBizToken());
        detectAuthJson.set("ruleId", ruleId);
        detectAuthJson.set("faceIdType", StrUtil.isNotBlank(imageBase64) ? FaceIdTypeEnum.LIVENESS_COMPARE.getCode() : FaceIdTypeEnum.LIVENESS_RECOGNITION.getCode());

        // 因为返回的BizToken的有效期为7200秒，按1.5倍的时间进行存储，减少因为缓存过期导致的验证失败
        cacheRedis.set(RedisConstant.Exam.ATTENDANCE_RECORD_DETECT_AUTH + ThreadContext.getUserId() + RedisConstant.SEPARATOR + bizId, detectAuthJson.toString(), 10800, TimeUnit.SECONDS);

        resp.setUrl(response.getUrl());
        resp.setBizId(bizId);
        return Result.success(resp);
    }

    /**
     * 处理模拟考试-实名认证地址
     *
     * @param req  请求参数
     * @param user 用户信息
     * @return 实名认证地址
     */
    private Result<AuthUserAttendanceDetectAuthUrlResp> handleRegistrationDetectAuthUrl(AuthUserAttendanceDetectAuthUrlReq req, Users user) {
        Exam exam = examDao.getByIdForCache(req.getRelateId());
        if (ObjectUtil.isNull(exam)) {
            return Result.error("考试不存在");
        }

        // 考试用户
        ExamUser examUser = examUserDao.getByExamIdAndUserId(req.getRelateId(), ThreadContext.getUserId());
        if (ObjectUtil.isNull(examUser)) {
            return Result.error("考试用户不存在");
        }

        // 开考前
        if (Boolean.FALSE.equals(exam.getFaceId())) {
            return Result.error("未开启考前人脸识别，无须进行人脸识别");
        }
        LocalDateTime faceStartTime = exam.getExamStartTime().minusMinutes(exam.getFaceIdLimitTime());
        if (faceStartTime.isAfter(LocalDateTime.now())) {
            return Result.error("未到达考前人脸识别开始时间，请稍后再试");
        }
        if (examUser.getFaceIdErrorCount() >= exam.getFaceIdLimitCount()) {
            return Result.error("未到达考前人脸识别开始时间，请稍后再试");
        }

        // 是否需要填写个人信息
        AuthUserAttendanceDetectAuthUrlResp resp = new AuthUserAttendanceDetectAuthUrlResp();
        resp.setCertification(!CertificationStatusEnum.SUCCESS_AUTH.getCode().equals(user.getCertificationStatus()));
        if (Boolean.TRUE.equals(resp.getCertification())) {
            return Result.success(resp);
        }

        SysConfigFaceResp configFace = sysConfigCommonBiz.getFace();
        // 限制单日最大错误次数
        if (Boolean.TRUE.equals(configFace.getFaceErrorEnable())) {
            Integer errorCount = getUserTodayFaceErrorCount(ThreadContext.getUserId());
            if (errorCount >= configFace.getFaceErrorLimit()) {
                return Result.error("今日达到错误次数超过限制，请明天再试");
            }
        }


        String ruleId = StrUtil.isNotBlank(user.getBestFrame()) ? configFace.getLivenessCompareRuleId() : configFace.getLivenessRecognitionRuleId();
        String imageBase64 = StrUtil.isNotBlank(user.getBestFrame()) ? FaceContrastUtil.encodeBase64File(user.getBestFrame()) : null;

        Long bizId = IdWorker.getId();
        JSONObject extraJson = new JSONObject();
        extraJson.set("bizId", bizId);
        extraJson.set("param", req);

        DetectAuthResponse response = FaceContrastUtil.detectAuth(
                configFace.getTencentFaceSecretId(),
                configFace.getTencentFaceSecretKey(),
                ruleId,
                user.getRealName(),
                user.getIdCard(),
                imageBase64,
                req.getRedirectUrl(),
                extraJson.toString());
        if (ObjectUtil.isNull(response)) {
            return Result.error("获取认证地址失败");
        }

        JSONObject detectAuthJson = new JSONObject();
        detectAuthJson.set("bizToken", response.getBizToken());
        detectAuthJson.set("ruleId", ruleId);
        detectAuthJson.set("faceIdType", StrUtil.isNotBlank(imageBase64) ? FaceIdTypeEnum.LIVENESS_COMPARE.getCode() : FaceIdTypeEnum.LIVENESS_RECOGNITION.getCode());

        // 因为返回的BizToken的有效期为7200秒，按1.5倍的时间进行存储，减少因为缓存过期导致的验证失败
        cacheRedis.set(RedisConstant.Exam.ATTENDANCE_RECORD_DETECT_AUTH + ThreadContext.getUserId() + RedisConstant.SEPARATOR + bizId, detectAuthJson.toString(), 10800, TimeUnit.SECONDS);

        resp.setUrl(response.getUrl());
        resp.setBizId(bizId);
        return Result.success(resp);
    }

    /**
     * 用户考勤-考勤信息校验
     *
     * @param req 请求参数
     * @return 校验结果
     */
    public Result<AuthUserAttendanceCheckResp> check(AuthUserAttendanceCheckReq req) {
        // 校验业务类型
        AttendanceTypeEnum attendanceTypeEnum = AttendanceTypeEnum.byCode(req.getAttendanceType());
        if (ObjectUtil.isNull(attendanceTypeEnum)) {
            return Result.error("考勤类型错误");
        }

        // 校验用户信息
        Users user = usersDao.getById(ThreadContext.getUserId());
        if (ObjectUtil.isNull(user)) {
            return Result.error("用户信息不存在");
        }

        if (ObjectUtil.isNull(req.getAttendanceClient())) {
            req.setAttendanceClient(AttendanceClientEnum.PC.getCode());
        }
        if (ObjectUtil.isNull(req.getAttendanceExamScene())) {
            req.setAttendanceExamScene(AttendanceExamSceneEnum.EXAM_ANSWER.getCode());
        }

        if (AttendanceTypeEnum.GENERAL_EXAM.equals(attendanceTypeEnum)) {
            // 模拟考试
            return handleGeneralExamCheck(req, user);
        } else if (AttendanceTypeEnum.REGISTRATION_EXAM.equals(attendanceTypeEnum)) {
            // 正式考试
            return handleRegistrationCheck(req, user);
        } else {
            return Result.error("该考勤类型暂不支持");
        }
    }

    /**
     * 用户考勤-模拟考试考勤校验
     *
     * @param req  请求参数
     * @param user 用户信息
     * @return 校验结果
     */
    private Result<AuthUserAttendanceCheckResp> handleGeneralExamCheck(AuthUserAttendanceCheckReq req, Users user) {
        Exam exam = examDao.getByIdForCache(req.getRelateId());
        if (ObjectUtil.isNull(exam)) {
            return Result.error("考试不存在");
        }

        if (AttendanceExamSceneEnum.EXAM_BEFORE.getCode().equals(req.getAttendanceExamScene())) {
            // 开考前
            if (Boolean.FALSE.equals(exam.getFaceId())) {
                return Result.error("未开启考前人脸识别，无须进行人脸识别");
            }

            // 校验识别视频
            if (StrUtil.isBlank(req.getContent())) {
                return Result.error("签到内容/视频不能为空");
            }

            if (ObjectUtil.isNotNull(req.getRecordId())) {
                ExamUserRecord examUserRecord = examUserRecordDao.getById(req.getRecordId());
                if (ObjectUtil.isNull(examUserRecord) || !ThreadContext.getUserId().equals(examUserRecord.getUserId())) {
                    return Result.error("考试用户记录不存在");
                }
                if (!ExamStatusEnum.NOT_OVER.getCode().equals(examUserRecord.getExamStatus())) {
                        return Result.error("考试已经结束");
                }
            }

            // 人脸识别配置
            SysConfigFaceResp configFace = sysConfigCommonBiz.getFace();
            // 限制单日最大错误次数
            if (Boolean.TRUE.equals(configFace.getFaceErrorEnable())) {
                Integer errorCount = getUserTodayFaceErrorCount(ThreadContext.getUserId());
                if (errorCount >= configFace.getFaceErrorLimit()) {
                    return Result.error("今日达到错误次数超过限制，请明天再试");
                }
            }

            // 获取人脸识别结果
            FaceCheckResult checkResult = getFaceCheckResult(user, req.getAttendanceClient(), req.getContent(), configFace);
            if (AttendanceClientEnum.WECHAT_H5.getCode().equals(req.getAttendanceClient())) {
                if (StrUtil.isNotBlank(checkResult.getExtra())) {
                    JSONObject extraJson = JSONUtil.parseObj(checkResult.getExtra());
                    AuthUserAttendanceDetectAuthUrlReq param = extraJson.getBean("param", AuthUserAttendanceDetectAuthUrlReq.class);
                    if (ObjectUtil.isNotNull(param)) {
                        Boolean checkRes = checkFaceCheckParam(param, req);
                        if (Boolean.FALSE.equals(checkRes)) {
                            return Result.error("无效参数");
                        }
                    }
                }
            }


            // 上传最佳照片
            String bestImage = updateImgBase64(checkResult.getBestFrameBase64());

            UserAttendance record = new UserAttendance()
                    .setUserId(ThreadContext.getUserId())
                    .setAttendanceType(req.getAttendanceType())
                    .setRelateId(req.getRelateId())
                    .setExamRecordId(req.getRecordId())
                    .setAttendanceExamScene(req.getAttendanceExamScene())
                    .setAttendanceWay(AttendanceWayEnum.FACE_ID.getCode())
                    .setAttendancePass(checkResult.getCheckPass())
                    .setFaceIdCall(Boolean.TRUE)
                    .setFaceIdType(checkResult.getFaceIdType())
                    .setFaceSim(checkResult.getSim())
                    .setUserImage(user.getUserHead())
                    .setBestImage(bestImage)
                    .setRespMsg(checkResult.getRespMsg())
                    .setGmtCreate(LocalDateTime.now());
            dao.save(record);

            // 处理返回信息
            return handleExamCheckResp(user, exam, record, configFace);
        } else if (AttendanceExamSceneEnum.EXAM_ANSWER.getCode().equals(req.getAttendanceExamScene())) {
            // 答题中
            if (ObjectUtil.isNull(req.getRecordId())) {
                return Result.error("考试记录ID不能为空");
            }
            if (ObjectUtil.isNotNull(req.getRecordId())) {
                ExamUserRecord examUserRecord = examUserRecordDao.getById(req.getRecordId());
                if (ObjectUtil.isNull(examUserRecord) || !ThreadContext.getUserId().equals(examUserRecord.getUserId())) {
                    return Result.error("考试用户记录不存在");
                }
                if (!ExamStatusEnum.NOT_OVER.getCode().equals(examUserRecord.getExamStatus())) {
                    return Result.error("考试已经结束");
                }
            }

            // 校验抓拍照片
            if (StrUtil.isBlank(req.getContent())) {
                return Result.error("抓拍人脸照片不能为空");
            }

            if (Boolean.FALSE.equals(exam.getFaceCapture())) {
                return Result.error("未开启人脸抓拍，无须进行抓拍识别");
            }

            SysConfigFaceResp configFace = sysConfigCommonBiz.getFace();

            Boolean checkPass = Boolean.TRUE;
            Boolean faceIdCall = Boolean.FALSE;
            Integer faceIdType = FaceIdTypeEnum.COMPARE_FACE.getCode();
            BigDecimal faceSim = BigDecimal.ZERO;
            String bestImage = "";
            String respMsg = "";
            if (Boolean.TRUE.equals(exam.getFaceCaptureDetection())) {
                // 开启抓拍比对
                UserAttendance beforeRecord = dao.lambdaQuery()
                        .eq(UserAttendance::getAttendanceType, req.getAttendanceType())
                        .eq(UserAttendance::getRelateId, req.getRelateId())
                        .eq(UserAttendance::getAttendanceWay, AttendanceWayEnum.FACE_ID.getCode())
                        .eq(UserAttendance::getAttendanceExamScene, AttendanceExamSceneEnum.EXAM_BEFORE.getCode())
                        .eq(UserAttendance::getAttendancePass, Boolean.TRUE)
                        .eq(UserAttendance::getUserId, ThreadContext.getUserId())
                        .eq(UserAttendance::getExamRecordId, req.getRecordId())
                        .orderByDesc(UserAttendance::getId)
                        .last(SqlUtil.limitOne())
                        .one();
                if (ObjectUtil.isNotNull(beforeRecord)) {
                    faceIdCall = Boolean.TRUE;

                    // 限制单日最大错误次数
                    if (Boolean.TRUE.equals(configFace.getFaceErrorEnable())) {
                        Integer errorCount = getUserTodayFaceErrorCount(ThreadContext.getUserId());
                        if (errorCount >= configFace.getFaceErrorLimit()) {
                            return Result.error("今日达到错误次数超过限制，请明天再试");
                        }
                    }

                    CompareFaceResponse compareFaceResponse = FaceContrastUtil.compareFace(configFace.getTencentFaceSecretId(), configFace.getTencentFaceSecretKey(), FaceContrastUtil.encodeBase64File(beforeRecord.getBestImage()), req.getContent());
                    if (compareFaceResponse != null) {
                        faceSim = BigDecimal.valueOf(compareFaceResponse.getScore());
                        checkPass = configFace.getFaceMinSim().compareTo(faceSim) <= 0;
                        bestImage = updateImgBase64(req.getContent());
                        respMsg = checkPass ? "答题前人脸照片比对答题时抓拍人脸照片成功" : "答题前人脸照片比对答题时抓拍人脸照片失败";
                    }
                }
            }

            UserAttendance record = new UserAttendance()
                    .setUserId(ThreadContext.getUserId())
                    .setAttendanceType(req.getAttendanceType())
                    .setRelateId(req.getRelateId())
                    .setExamRecordId(req.getRecordId())
                    .setAttendanceExamScene(req.getAttendanceExamScene())
                    .setAttendanceWay(AttendanceWayEnum.FACE_ID.getCode())
                    .setAttendancePass(checkPass)
                    .setFaceIdCall(faceIdCall)
                    .setFaceIdType(faceIdType)
                    .setFaceSim(faceSim)
                    .setUserImage(user.getBestFrame())
                    .setBestImage(bestImage)
                    .setRespMsg(respMsg)
                    .setGmtCreate(LocalDateTime.now());
            dao.save(record);

            // 处理响应结果
            return handleExamCheckResp(user, exam, record, configFace);
        } else {
            // 答题时高灵敏度人脸监测
            if (ObjectUtil.isNull(req.getRecordId())) {
                return Result.error("考试记录ID不能为空");
            }
            if (ObjectUtil.isNotNull(req.getRecordId())) {
                ExamUserRecord examUserRecord = examUserRecordDao.getById(req.getRecordId());
                if (ObjectUtil.isNull(examUserRecord) || !ThreadContext.getUserId().equals(examUserRecord.getUserId())) {
                    return Result.error("考试用户记录不存在");
                }
                if (!ExamStatusEnum.NOT_OVER.getCode().equals(examUserRecord.getExamStatus())) {
                    return Result.error("考试已经结束");
                }
            }

            // 答题时高灵敏度人脸监测(前端负责校验，该接口只保存错误记录信息)
            if (Boolean.FALSE.equals(exam.getFaceId()) || Boolean.FALSE.equals(exam.getFaceCapture()) || Boolean.FALSE.equals(exam.getFaceCaptureDetection()) || Boolean.FALSE.equals(exam.getFaceCaptureHighDetection())) {
                return Result.error("答题前人脸照片比对答题时抓拍人脸照片失败");
            }

            UserAttendance record = new UserAttendance()
                    .setUserId(ThreadContext.getUserId())
                    .setAttendanceType(req.getAttendanceType())
                    .setRelateId(req.getRelateId())
                    .setExamRecordId(req.getRecordId())
                    .setAttendanceExamScene(req.getAttendanceExamScene())
                    .setAttendanceWay(AttendanceWayEnum.FACE_ID.getCode())
                    .setAttendancePass(Boolean.FALSE)
                    .setFaceIdCall(Boolean.TRUE)
                    .setFaceIdType(FaceIdTypeEnum.LIVENESS_COMPARE.getCode())
                    .setFaceSim(BigDecimal.ZERO)
                    .setUserImage(user.getUserHead())
                    .setBestImage("")
                    .setRespMsg("")
                    .setGmtCreate(LocalDateTime.now());
            dao.save(record);

            // 处理返回信息
            SysConfigFaceResp configFace = sysConfigCommonBiz.getFace();
            return handleExamCheckResp(user, exam, record, configFace);
        }
    }

    /**
     * 用户考勤-正式考试考勤校验
     *
     * @param req  请求参数
     * @param user 用户信息
     * @return 校验结果
     */
    private Result<AuthUserAttendanceCheckResp> handleRegistrationCheck(AuthUserAttendanceCheckReq req, Users user) {
        Exam exam = examDao.getByIdForCache(req.getRelateId());
        if (ObjectUtil.isNull(exam)) {
            return Result.error("考试不存在");
        }

        // 考试用户
        ExamUser examUser = examUserDao.getByExamIdAndUserId(req.getRelateId(), ThreadContext.getUserId());
        if (ObjectUtil.isNull(examUser)) {
            return Result.error("考试用户不存在");
        }

        if (AttendanceExamSceneEnum.EXAM_BEFORE.getCode().equals(req.getAttendanceExamScene())) {
            // 开考前
            if (Boolean.FALSE.equals(exam.getFaceId())) {
                return Result.error("未开启考前人脸识别，无须进行人脸识别");
            }
            LocalDateTime faceStartTime = exam.getExamStartTime().minusMinutes(exam.getFaceIdLimitTime());
            if (faceStartTime.isAfter(LocalDateTime.now())) {
                return Result.error("未到达考前人脸识别开始时间，请稍后再试");
            }
            if (examUser.getFaceIdErrorCount() >= exam.getFaceIdLimitCount()) {
                return Result.error("人脸识别失败次数达到最大限制，无法进行正常考试");
            }

            // 校验识别视频
            if (StrUtil.isBlank(req.getContent())) {
                return Result.error("签到内容/视频不能为空");
            }

            SysConfigFaceResp configFace = sysConfigCommonBiz.getFace();
            // 限制单日最大错误次数
            if (Boolean.TRUE.equals(configFace.getFaceErrorEnable())) {
                Integer errorCount = getUserTodayFaceErrorCount(ThreadContext.getUserId());
                if (errorCount >= configFace.getFaceErrorLimit()) {
                    return Result.error("今日达到错误次数超过限制，请明天再试");
                }
            }

            // 获取人脸识别结果
            FaceCheckResult checkResult = getFaceCheckResult(user, req.getAttendanceClient(), req.getContent(), configFace);
            if (AttendanceClientEnum.WECHAT_H5.getCode().equals(req.getAttendanceClient())) {
                if (StrUtil.isNotBlank(checkResult.getExtra())) {
                    JSONObject extraJson = JSONUtil.parseObj(checkResult.getExtra());
                    AuthUserAttendanceDetectAuthUrlReq param = extraJson.getBean("param", AuthUserAttendanceDetectAuthUrlReq.class);
                    if (ObjectUtil.isNotNull(param)) {
                        Boolean checkRes = checkFaceCheckParam(param, req);
                        if (Boolean.FALSE.equals(checkRes)) {
                            return Result.error("无效参数");
                        }
                    }
                }
            }

            // 上传最佳照片
            String bestImage = updateImgBase64(checkResult.getBestFrameBase64());

            UserAttendance record = new UserAttendance()
                    .setUserId(ThreadContext.getUserId())
                    .setAttendanceType(req.getAttendanceType())
                    .setRelateId(req.getRelateId())
                    .setExamRecordId(req.getRecordId())
                    .setAttendanceExamScene(req.getAttendanceExamScene())
                    .setAttendanceWay(AttendanceWayEnum.FACE_ID.getCode())
                    .setAttendancePass(checkResult.getCheckPass())
                    .setFaceIdCall(Boolean.TRUE)
                    .setFaceIdType(checkResult.getFaceIdType())
                    .setFaceSim(checkResult.getSim())
                    .setUserImage(user.getUserHead())
                    .setBestImage(bestImage)
                    .setRespMsg(checkResult.getRespMsg())
                    .setGmtCreate(LocalDateTime.now());
            dao.save(record);

            // 更新考试用户考试开考前人脸识别错误次数
            if (Boolean.FALSE.equals(checkResult.getCheckPass())) {
                handleExamUserFaceError(examUser, Boolean.FALSE);
            }

            // 处理返回信息
            return handleExamCheckResp(user, exam, record, configFace);
        } else if (AttendanceExamSceneEnum.EXAM_ANSWER.getCode().equals(req.getAttendanceExamScene())) {
            // 答题时抓拍
            // 校验抓拍照片
            if (StrUtil.isBlank(req.getContent())) {
                return Result.error("抓拍人脸照片不能为空");
            }

            if (Boolean.FALSE.equals(exam.getFaceCapture())) {
                return Result.error("未开启人脸抓拍，无须进行抓拍识别");
            }

            SysConfigFaceResp configFace = sysConfigCommonBiz.getFace();

            Boolean checkPass = Boolean.TRUE;
            Boolean faceIdCall = Boolean.FALSE;
            Integer faceIdType = FaceIdTypeEnum.COMPARE_FACE.getCode();
            BigDecimal faceSim = BigDecimal.ZERO;
            String bestImage = "";
            String respMsg = "";
            if (Boolean.TRUE.equals(exam.getFaceCaptureDetection())) {
                // 开启抓拍比对
                UserAttendance beforeRecord = dao.lambdaQuery()
                        .eq(UserAttendance::getAttendanceType, req.getAttendanceType())
                        .eq(UserAttendance::getRelateId, req.getRelateId())
                        .eq(UserAttendance::getAttendanceWay, AttendanceWayEnum.FACE_ID.getCode())
                        .eq(UserAttendance::getAttendanceExamScene, AttendanceExamSceneEnum.EXAM_BEFORE.getCode())
                        .eq(UserAttendance::getAttendancePass, Boolean.TRUE)
                        .eq(UserAttendance::getUserId, ThreadContext.getUserId())
                        .orderByDesc(UserAttendance::getId)
                        .last(SqlUtil.limitOne())
                        .one();
                if (ObjectUtil.isNotNull(beforeRecord)) {
                    faceIdCall = Boolean.TRUE;

                    // 限制单日最大错误次数
                    if (Boolean.TRUE.equals(configFace.getFaceErrorEnable())) {
                        Integer errorCount = getUserTodayFaceErrorCount(ThreadContext.getUserId());
                        if (errorCount >= configFace.getFaceErrorLimit()) {
                            return Result.error("今日达到错误次数超过限制，请明天再试");
                        }
                    }

                    CompareFaceResponse compareFaceResponse = FaceContrastUtil.compareFace(configFace.getTencentFaceSecretId(), configFace.getTencentFaceSecretKey(), FaceContrastUtil.encodeBase64File(beforeRecord.getBestImage()), req.getContent());
                    if (compareFaceResponse != null) {
                        faceSim = BigDecimal.valueOf(compareFaceResponse.getScore());
                        checkPass = configFace.getFaceMinSim().compareTo(faceSim) <= 0;
                        bestImage = updateImgBase64(req.getContent());
                        respMsg = checkPass ? "答题前人脸照片比对答题时抓拍人脸照片成功" : "答题前人脸照片比对答题时抓拍人脸照片失败";
                    }
                }
            }

            UserAttendance record = new UserAttendance()
                    .setUserId(ThreadContext.getUserId())
                    .setAttendanceType(req.getAttendanceType())
                    .setRelateId(req.getRelateId())
                    .setExamRecordId(examUser.getExamRecordId())
                    .setAttendanceExamScene(req.getAttendanceExamScene())
                    .setAttendanceWay(AttendanceWayEnum.FACE_ID.getCode())
                    .setAttendancePass(checkPass)
                    .setFaceIdCall(faceIdCall)
                    .setFaceIdType(faceIdType)
                    .setFaceSim(faceSim)
                    .setUserImage(user.getBestFrame())
                    .setBestImage(bestImage)
                    .setRespMsg(respMsg)
                    .setGmtCreate(LocalDateTime.now());
            dao.save(record);

            // 更新人脸抓拍错误次数
            if (!checkPass) {
                handleExamUserFaceError(examUser, Boolean.TRUE);
            }

            return handleExamCheckResp(user, exam, record, configFace);
        } else {
            // 答题时高灵敏度人脸监测(前端负责校验，该接口只保存错误记录信息)
            if (Boolean.FALSE.equals(exam.getFaceId()) || Boolean.FALSE.equals(exam.getFaceCapture()) || Boolean.FALSE.equals(exam.getFaceCaptureDetection()) || Boolean.FALSE.equals(exam.getFaceCaptureHighDetection())) {
                return Result.error("答题前人脸照片比对答题时抓拍人脸照片失败");
            }

            UserAttendance record = new UserAttendance()
                    .setUserId(ThreadContext.getUserId())
                    .setAttendanceType(req.getAttendanceType())
                    .setRelateId(req.getRelateId())
                    .setExamRecordId(examUser.getExamRecordId())
                    .setAttendanceExamScene(req.getAttendanceExamScene())
                    .setAttendanceWay(AttendanceWayEnum.FACE_ID.getCode())
                    .setAttendancePass(Boolean.FALSE)
                    .setFaceIdCall(Boolean.TRUE)
                    .setFaceIdType(FaceIdTypeEnum.LIVENESS_COMPARE.getCode())
                    .setFaceSim(BigDecimal.ZERO)
                    .setUserImage(user.getUserHead())
                    .setBestImage("")
                    .setRespMsg("")
                    .setGmtCreate(LocalDateTime.now());
            dao.save(record);

            // 更新人脸抓拍错误次数
            handleExamUserFaceError(examUser, Boolean.TRUE);

            // 处理返回信息
            SysConfigFaceResp configFace = sysConfigCommonBiz.getFace();
            return handleExamCheckResp(user, exam, record, configFace);
        }
    }

    /**
     * 处理考试用户人脸错误次数
     *
     * @param examUser 考试用户
     */
    private void handleExamUserFaceError(ExamUser examUser, Boolean faceCapture) {
        examUserDao.lambdaUpdate()
                .eq(ExamUser::getId, examUser.getId())
                // 人脸抓拍检测错误次数
                .setSql(Boolean.TRUE.equals(faceCapture), "face_capture_error_count = face_capture_error_count + 1")
                // 考试开考前人脸识别错误次数
                .setSql(Boolean.FALSE.equals(faceCapture), "face_id_error_count = face_id_error_count + 1")
                .update();
    }

    /**
     * 校验人脸检测参数
     *
     * @param urlReq   获取认证路径参数
     * @param checkReq 检测校验参数
     * @return 校验结果
     */
    private Boolean checkFaceCheckParam(AuthUserAttendanceDetectAuthUrlReq urlReq, AuthUserAttendanceCheckReq checkReq) {
        // 校验考勤类型
        if (!Objects.equals(urlReq.getAttendanceType(), checkReq.getAttendanceType())) {
            return Boolean.FALSE;
        }
        // 校验关联ID
        if (!Objects.equals(urlReq.getRelateId(), checkReq.getRelateId())) {
            return Boolean.FALSE;
        }
        // 校验课时
        if (ObjectUtil.isNotNull(checkReq.getPeriodId())) {
            if (!Objects.equals(urlReq.getPeriodId(), checkReq.getPeriodId())) {
                return Boolean.FALSE;
            }
        }
        // 不是开考前不能调用
        if (AttendanceTypeEnum.GENERAL_EXAM.getCode().equals(checkReq.getAttendanceType()) || AttendanceTypeEnum.REGISTRATION_EXAM.getCode().equals(checkReq.getAttendanceType())) {
            if (!AttendanceExamSceneEnum.EXAM_BEFORE.getCode().equals(checkReq.getAttendanceExamScene())) {
                return Boolean.FALSE;
            }
        }
        return Boolean.TRUE;
    }

    /**
     * 处理考试人脸识别校验结果
     *
     * @param user       用户信息
     * @param exam       考试信息
     * @param record     用户考勤
     * @param configFace 人脸识别配置
     * @return 结果
     */
    private Result<AuthUserAttendanceCheckResp> handleExamCheckResp(Users user, Exam exam, UserAttendance record, SysConfigFaceResp configFace) {
        // 处理考试配置
        AuthUserAttendanceCheckResp.FaceConfig faceConfig = new AuthUserAttendanceCheckResp.FaceConfig()
                .setCertification(CertificationStatusEnum.SUCCESS_AUTH.getCode().equals(user.getCertificationStatus()))
                .setEnabledFaceError(configFace.getFaceErrorEnable())
                .setFaceErrorCount(configFace.getFaceErrorLimit())
                .setFaceId(exam.getFaceId())
                .setFaceIdLimitTime(exam.getFaceIdLimitTime())
                .setFaceIdLimitCount(exam.getFaceIdLimitCount())
                .setFaceIdFailureEnter(exam.getFaceIdFailureEnter())
                .setFaceIdFailureAppeal(exam.getFaceIdFailureAppeal())
                .setFaceIdCheckPass(record.getAttendancePass())
                .setFaceCapture(exam.getFaceCapture())
                .setFaceCaptureTime(exam.getFaceCaptureTime())
                .setFaceCaptureDetection(exam.getFaceCaptureDetection())
                .setFaceCaptureHighDetection(exam.getFaceCaptureHighDetection())
                .setFaceCaptureFailureCount(exam.getFaceCaptureFailureCount());

        if (Boolean.TRUE.equals(exam.getFaceId())) {
            if (Boolean.FALSE.equals(record.getAttendancePass())) {
                // 是否已通过人脸识别
                boolean faceIdCheckPass = dao.lambdaQuery()
                        .eq(UserAttendance::getAttendanceType, record.getAttendanceType())
                        .eq(UserAttendance::getRelateId, record.getRelateId())
                        .eq(UserAttendance::getAttendanceWay, record.getAttendanceWay())
                        .eq(UserAttendance::getAttendanceExamScene, AttendanceExamSceneEnum.EXAM_BEFORE.getCode())
                        .eq(UserAttendance::getUserId, ThreadContext.getUserId())
                        .eq(UserAttendance::getAttendancePass, Boolean.TRUE)
                        .exists();
                faceConfig.setFaceIdCheckPass(faceIdCheckPass);
            }

            // 错误次数统计
            if (AttendanceTypeEnum.GENERAL_EXAM.getCode().equals(record.getAttendanceType()) && ObjectUtil.isNull(record.getExamRecordId())) {
                faceConfig.setFaceIdUserErrorCount(0);
            } else {
                Long faceIdUserErrorCount = dao.lambdaQuery()
                        .eq(UserAttendance::getAttendanceType, record.getAttendanceType())
                        .eq(UserAttendance::getRelateId, record.getRelateId())
                        .eq(UserAttendance::getAttendanceWay, record.getAttendanceWay())
                        .eq(UserAttendance::getAttendanceExamScene, AttendanceExamSceneEnum.EXAM_BEFORE.getCode())
                        .eq(UserAttendance::getUserId, ThreadContext.getUserId())
                        .eq(UserAttendance::getAttendancePass, Boolean.FALSE)
                        .count();
                faceConfig.setFaceIdUserErrorCount(faceIdUserErrorCount.intValue());
            }

            // 统计当日错误次数
            int faceUserTodayErrorCount = getUserTodayFaceErrorCount(ThreadContext.getUserId());
            faceConfig.setFaceUserTodayErrorCount(faceUserTodayErrorCount);
        } else {
            faceConfig.setFaceIdCheckPass(Boolean.TRUE);
            faceConfig.setFaceIdUserErrorCount(0);
        }

        // 人脸抓拍检测
        if (Boolean.TRUE.equals(exam.getFaceCaptureDetection())) {
            if (AttendanceTypeEnum.GENERAL_EXAM.getCode().equals(record.getAttendanceType()) && ObjectUtil.isNull(record.getExamRecordId())) {
                faceConfig.setFaceCaptureUserFailureCount(0);
            } else {
                Long faceCaptureUserFailureCount = dao.lambdaQuery()
                        .eq(UserAttendance::getAttendanceType, record.getAttendanceType())
                        .eq(UserAttendance::getRelateId, record.getRelateId())
                        .eq(UserAttendance::getAttendanceWay, AttendanceWayEnum.FACE_ID.getCode())
                        .in(UserAttendance::getAttendanceExamScene, AttendanceExamSceneEnum.EXAM_ANSWER.getCode(), AttendanceExamSceneEnum.EXAM_ANSWER_COMPARE.getCode())
                        .eq(UserAttendance::getUserId, ThreadContext.getUserId())
                        .eq(UserAttendance::getAttendancePass, Boolean.FALSE)
                        .count();
                faceConfig.setFaceCaptureUserFailureCount(faceCaptureUserFailureCount.intValue());
            }
        } else {
            faceConfig.setFaceCaptureUserFailureCount(0);
        }

        // 异常审核状态处理
        if (ExamTypeEnum.EXAMINATION.getCode().equals(exam.getExamType())) {
            ExamUserAppeal errorAppeal = examUserAppealDao.lambdaQuery()
                    .eq(ExamUserAppeal::getExamId, exam.getId())
                    .eq(ExamUserAppeal::getUserId, ThreadContext.getUserId())
                    .orderByDesc(ExamUserAppeal::getId)
                    .last(SqlUtil.limitOne())
                    .one();
            if (ObjectUtil.isNotNull(errorAppeal)) {
                faceConfig.setAppealStatus(errorAppeal.getAppealStatus())
                        .setAppealCause(errorAppeal.getAppealCause())
                        .setAuditTime(errorAppeal.getAuditTime())
                        .setAuditOpinion(errorAppeal.getAuditOpinion());
            }
        }


        AuthUserAttendanceCheckResp resp = new AuthUserAttendanceCheckResp()
                .setCheckPass(record.getAttendancePass())
                .setAttendanceType(record.getAttendanceType())
                .setRelateId(record.getRelateId())
                .setAttendanceWay(AttendanceWayEnum.FACE_ID.getCode())
                .setFaceConfig(faceConfig);
        return Result.success(resp);
    }

    /**
     * 获取人脸识别结果
     *
     * @param user             用户信息
     * @param attendanceClient 考勤客户端
     * @param content          考勤内容
     * @param configFace       人脸识别配置
     * @return 人脸识别结果
     */
    private FaceCheckResult getFaceCheckResult(Users user, Integer attendanceClient, String content, SysConfigFaceResp configFace) {
        FaceCheckResult result = new FaceCheckResult()
                .setCheckPass(Boolean.FALSE)
                .setFaceIdType(FaceIdTypeEnum.LIVENESS_COMPARE.getCode())
                .setSim(BigDecimal.ZERO)
                .setRespMsg("");

        if (AttendanceClientEnum.APP.getCode().equals(attendanceClient)) {
            // APP端
            FaceQueryRecordResult recordResult = FaceContrastUtil.queryFaceRecord(configFace.getTencentFaceWbAppid(), configFace.getTencentFaceWbAppidSecret(), content, "2");
            if (recordResult != null) {
                if (ObjectUtil.isNotNull(recordResult.getSimilarity())) {
                    BigDecimal sim = new BigDecimal(recordResult.getSimilarity());
                    result.setSim(sim);
                }
                if (StrUtil.isNotBlank(recordResult.getPhoto())) {
                    result.setBestFrameBase64(recordResult.getPhoto());
                }

                // 清空最佳照片，防止记录内容过大
                recordResult.setPhoto(null);
                recordResult.setVideo(null);
                result.setRespMsg(JSONUtil.toJsonStr(recordResult));
            }
        } else if (AttendanceClientEnum.WECHAT_H5.getCode().equals(attendanceClient)) {
            // 微信H5端
            String detectAuthStr = cacheRedis.get(RedisConstant.Exam.ATTENDANCE_RECORD_DETECT_AUTH + ThreadContext.getUserId() + RedisConstant.SEPARATOR + content);
            if (StrUtil.isBlank(detectAuthStr)) {
                throw new BaseException("业务ID已失效");
            }
            JSONObject detectAuthJson = JSONUtil.parseObj(detectAuthStr);

            String bizToken = detectAuthJson.getStr("bizToken");
            String ruleId = detectAuthJson.getStr("ruleId");

            Integer faceIdType = detectAuthJson.getInt("faceIdType");
            if (ObjectUtil.isNotNull(faceIdType)) {
                result.setFaceIdType(faceIdType);
            }

            GetDetectInfoEnhancedResponse detectInfoEnhanced = FaceContrastUtil.getDetectInfoEnhanced(configFace.getTencentFaceSecretId(), configFace.getTencentFaceSecretKey(), ruleId, bizToken);
            if (detectInfoEnhanced != null) {
                DetectInfoText detectInfoText = detectInfoEnhanced.getText();
                if (ObjectUtil.isNotNull(detectInfoText)) {
                    if (StrUtil.isNotBlank(detectInfoText.getSim())) {
                        BigDecimal sim = new BigDecimal(detectInfoEnhanced.getText().getSim());
                        result.setSim(sim);
                    }
                    if (StrUtil.isNotBlank(detectInfoText.getExtra())) {
                        result.setExtra(detectInfoText.getExtra());
                    }
                }
                if (ObjectUtil.isNotNull(detectInfoEnhanced.getBestFrame()) && StrUtil.isNotBlank(detectInfoEnhanced.getBestFrame().getBestFrame())) {
                    result.setBestFrameBase64(detectInfoEnhanced.getBestFrame().getBestFrame());
                }

                // 清空最佳照片，防止记录内容过大
                detectInfoEnhanced.setBestFrame(null);
                detectInfoEnhanced.setEncryption(null);
                detectInfoEnhanced.setEncryptedBody(null);
                detectInfoEnhanced.setIdCardData(null);
                detectInfoEnhanced.setVideoData(null);
                result.setRespMsg(JSONUtil.toJsonStr(detectInfoEnhanced));
            }
        } else {
            // 其他
            LivenessCompareResponse compareResponse = FaceContrastUtil.livenessCompare(configFace.getTencentFaceSecretId(), configFace.getTencentFaceSecretKey(), FaceContrastUtil.encodeBase64File(user.getBestFrame()), content);
            if (compareResponse != null) {
                if (ObjectUtil.isNotNull(compareResponse.getSim())) {
                    result.setSim(BigDecimal.valueOf(compareResponse.getSim()));
                }
                if (StrUtil.isNotBlank(compareResponse.getBestFrameBase64())) {
                    result.setBestFrameBase64(compareResponse.getBestFrameBase64());
                }


                // 清空最佳照片，防止记录内容过大
                compareResponse.setBestFrameBase64(null);
                compareResponse.setBestFrameList(null);
                result.setRespMsg(JSONUtil.toJsonStr(compareResponse));
            }
        }


        result.setCheckPass(configFace.getFaceMinSim().compareTo(result.getSim()) <= 0);
        return result;
    }

    /**
     * 上传图片Base64字符串
     *
     * @param imgBase64 图片Base64字符串
     * @return 图片地址
     */
    private String updateImgBase64(String imgBase64) {
        // 上传最佳照片
        Upload upload = sysConfigCommonBiz.getUpload();
        return uploadService.uploadPicBase64(imgBase64, upload);
    }

    /**
     * 人脸识别结果
     */
    @Accessors(chain = true)
    @Data
    private static class FaceCheckResult implements Serializable {

        @Serial
        private static final long serialVersionUID = -1292920062767855734L;

        /**
         * 校验是否通过
         */
        private Boolean checkPass;

        /**
         * 人脸识别类型(1:活体人脸核身、2:活体人脸比对)
         */
        private Integer faceIdType;

        /**
         * 相似度，取值范围 [0.00, 100.00]。推荐相似度大于等于70时可判断为同一人，可根据具体场景自行调整阈值（阈值70的误通过率为千分之一，阈值80的误通过率是万分之一）。
         */
        private BigDecimal sim;

        /**
         * 验证通过后的视频最佳截图照片，照片为BASE64编码后的值
         */
        private String bestFrameBase64;

        /**
         * 响应信息
         */
        private String respMsg;

        /**
         * 拓展信息（透传给人脸识别接口参数）
         */
        private String extra;
    }

}
